// 
// Entify project
//  
// Author:
//       Lauri Taimila <lauri@taimila.com>
// 
// Copyright (c) 2010 Lauri Taimila
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Text;
using System.Linq;
using System.Diagnostics;
using System.Collections.Generic;

using Mono.Data.Sqlite;

namespace Taimila.Entify.Sqlite
{
	/// <summary>
	/// TypeCoordinator's responsibility is to know all entity types
	/// stored into the persistent store. It keeps book of entity types 
	/// and their versions. TypeCoordinator also creates tables for new
	/// types and updates them as needed.
	/// </summary>
	internal class SqliteTypeCoordinator : ITypeCoordinator
	{	
		/// <summary>
		/// Contains all known entity types and their latest versions.
		/// Type is string representation of type including full namespace.
		/// value is latest version number of known type.
		/// </summary>
		private Dictionary<string, double> knownTypes;
		
		/// <summary>
		/// Database connection to persistent store.
		/// </summary>
		private SqliteConnection connection;
		
		/// <summary>
		/// Table name for known entities index table.
		/// </summary>
		private readonly string KnownEntitiesTable = "Taimila.Entify.Sqlite.SqliteTypeCoordinator.KnownEntityTypes";
		
		/// <summary>
		/// Table name for property index table.
		/// </summary>
		private readonly string KnownPropertiesTable = "Taimila.Entify.Sqlite.SqliteTypeCoordinator.KnownEntityProperties";
		
		/// <summary>
		/// Creates a new type coordinator object.
		/// </summary>
		public SqliteTypeCoordinator(SqliteConnection connection)
		{
			this.connection = connection;
			
			if(this.TableExists(this.KnownEntitiesTable) && this.TableExists(this.KnownPropertiesTable))
			{
				this.knownTypes = this.LoadKnownTypesFromDatabase();
			}
			else
			{
				this.CreateIndexTables();
				this.knownTypes = new Dictionary<string, double>();
			}
		}
		
		/// <summary>
		/// Checks is given type known entity type.
		/// </summary>
		/// <param name="type">
		/// Type to be checked. <see cref="System.Type"/>
		/// </param>
		/// <returns>
		/// True if it's known type, otherwise false. <see cref="System.Boolean"/>
		/// </returns>
		public bool IsKnownType(Type type)
		{
			if(type == null)
			{
				return false;	
			}
			else
			{
				return this.knownTypes.ContainsKey(type.FullName);
			}
		}
		
		/// <summary>
		/// Checks is type's entity version is same as currently known
		/// version of the entity type. 
		/// </summary>
		/// <param name="type">
		/// Type to be checked. <see cref="System.Type"/>
		/// </param>
		/// <returns>
		/// True if type is known version, otherwise false. <see cref="System.Boolean"/>
		/// </returns>
		public bool IsCurrentVersion(Type type)
		{
			double version = AttributeReader.GetEntityVersion(type);
			
			if(this.knownTypes[type.FullName] == version)
			{
				return true;	
			}
			else
			{
				return false;
			}
		}
		
		/// <summary>
		/// Adds new type to the persistent store.
		/// </summary>
		/// <param name="type">
		/// Type to be added. <see cref="System.Type"/>
		/// </param>
		public void AddType(Type type)
		{
			if(type == null)
			{
				throw new ArgumentNullException();
			}
			
			if(this.IsKnownType(type))
			{
				throw new AlreadyExistsException("Entity type " + type.FullName + " is already known.");	
			}
			
			this.knownTypes.Add(type.FullName, AttributeReader.GetEntityVersion(type));
			this.AddTypeToIndexTables(type);
			this.CreateTableForType(type);
			this.CreateIndexesForType(type);
		}

		/// <summary>
		/// Mark relation between given types with given relation name. This creates a new trigger
		/// to SQLite database. Trigger removes entries from relation table when targer entity is
		/// removed from the persistent store.
		/// </summary>
		/// <param name="relatedType">
		/// Entity type that is related from another entity type. <see cref="Type"/>
		/// </param>
		/// <param name="relatingType">
		/// Entity type that has relation to related type. <see cref="Type"/>
		/// </param>
		/// <param name="relationPropertyName">
		/// Name of the relation property on relating type. <see cref="System.String"/>
		/// </param>
		public void MarkAsRelatedType(Type relatedType, Type relatingType, string relationPropertyName)
		{
			using(SqliteCommand command = (SqliteCommand) this.connection.CreateCommand())
			{
				command.CommandText = this.GetCascadeDeleteTriggerQueryFor(relatedType, relatingType, relationPropertyName);
				EntifyLogger.LogQuery(command.GetQuery());
				command.ExecuteNonQuery();
			}
		}
		
		/// <summary>
		/// Updates existing entity type.
		/// </summary>
		/// <param name="type">
		/// Type to be updated. <see cref="System.Type"/>
		/// </param>
		public void UpdateType(Type type)
		{
			if(!this.IsKnownType(type))
			{
				throw new InvalidEntityTypeException("Unknown type. Type must be added before it can be updated.");	
			}
			
			if(this.IsCurrentVersion(type))
			{
				throw new InvalidEntityTypeException("Entity version is same as known version. Nothing to update");
			}
			
			// Update in-memory cache to have new version number.
			this.knownTypes[type.FullName] = AttributeReader.GetEntityVersion(type);
			
			//TODO: Update Index tables here.
			//TODO: Update type table here. (Add remove columns etc.)
		}
		
		/// <summary>
		/// Creates table for given type.
		/// </summary>
		/// <param name="t">
		/// Creates table for this type. <see cref="Type"/>
		/// </param>
		private void CreateTableForType(Type t)
		{
			if(t == null)
			{
				throw new ArgumentNullException();	
			}

			using(SqliteCommand command = (SqliteCommand) this.connection.CreateCommand())
			{
				try
				{
					command.CommandText = this.GetCreateTableQueryForType(t) + "\n" + this.GetCreateRelationTableQueriesForType(t);
					EntifyLogger.LogQuery(command.GetQuery());
					command.ExecuteNonQuery();
				}
				catch(SqliteException e)
				{
					if(e.Message.EndsWith("already exists")) // TODO: Better way to detect specific error?
					{
						throw new AlreadyExistsException(e.Message);
					}
					else
					{
						throw e;	
					}
				}
			}
		}
		
		/// <summary>
		/// Creates table indexes for given type.
		/// </summary>
		/// <param name="type">
		/// Type which table is updated. <see cref="Type"/>
		/// </param>
		private void CreateIndexesForType(Type type)
		{	
			Dictionary<string, List<string>> indexes = new Dictionary<string, List<string>>();
			
			foreach(var property in SqlitePersistentProperty.GetPersistentProperties(type))
			{
				if(property.IndexName != null)
				{
					if(indexes.ContainsKey(property.IndexName))
					{
						indexes[property.IndexName].Add(property.PersistentName);
					}
					else
					{
						indexes.Add(property.IndexName, new List<string>() { property.PersistentName });
					}
				}
			}
			
			List<string> indexQueries = new List<string>();
			
			foreach(var index in indexes)
			{
				indexQueries.Add("CREATE INDEX [" + index.Key + "] ON [" + type.FullName + "](" + string.Join(",", index.Value.ToArray()) + ")");	
			}

			using(SqliteTransaction transaction = (SqliteTransaction) this.connection.BeginTransaction())
			{
				using(SqliteCommand command = (SqliteCommand) this.connection.CreateCommand())
				{
					foreach(var query in indexQueries)
					{
						
						command.CommandText = query;
						EntifyLogger.LogQuery(command.GetQuery());
						command.ExecuteNonQuery();
					}
				}
				
				transaction.Commit();
			}
		}
		
		/// <summary>
		/// Get Create table SQL query for given type.
		/// </summary>
		/// <param name="type">
		/// Entity type. <see cref="Type"/>
		/// </param>
		/// <returns>
		/// SQL query for creating table for specified type. <see cref="System.String"/>
		/// </returns>
		private string GetCreateTableQueryForType (Type type)
		{
			List<string> columns = new List<string>();
			
			// Add special columns for framework's internal use.
			columns.Add("[Entity Id] INTEGER PRIMARY KEY");
			columns.Add("[Entity Version] REAL NOT NULL");
			columns.Add("[Entity Last Update] INTEGER NOT NULL");
			
			// Create database table column for each property.
			foreach(var property in SqlitePersistentProperty.GetPersistentProperties(type))
			{
				if(!property.IsManyRelation)
				{
					string column = property.PersistentName + " " + property.SqliteType;
					
					if(property.IsUnique)
					{
						column += " UNIQUE";	
					}
					
					columns.Add(column);
				}
			}

			StringBuilder query = new StringBuilder();
			query.AppendLine("CREATE TABLE [" + type.FullName + "]");
			query.AppendLine("(");
			query.AppendLine("\t" + string.Join(",\n\t",columns.ToArray()));
			query.AppendLine(");");

			return query.ToString();
		}

		/// <summary>
		/// Gets SQL query that creates a new cascade delete trigger for specified values.
		/// </summary>
		/// <param name="relatedType">
		/// Entity type which trigger is attached to. <see cref="Type"/>
		/// </param>
		/// <param name="relatingType">
		/// Entity type that has relation to related type. <see cref="Type"/>
		/// </param>
		/// <param name="relationPropertyName">
		/// Name of the relation. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// SQL query that creates a new trigger. <see cref="System.String"/>
		/// </returns>
		private string GetCascadeDeleteTriggerQueryFor(Type relatedType, Type relatingType, string relationPropertyName)
		{
			string triggerName = "[" + relatingType + "." + relationPropertyName +  "." + relatedType.FullName + ".CascadeDelete]";
			string masterTable = "[" + relatedType.FullName + "]";
			string relationTable = "[" + relatingType.FullName + "." + relationPropertyName + "]";

			return @"
					CREATE TRIGGER " + triggerName + @"
					BEFORE DELETE ON " + masterTable + @"
					FOR EACH ROW
					BEGIN
						DELETE FROM " + relationTable + @" WHERE " + relationTable + @".[Destination Id] = old.[Entity Id];
					END
					";
		}
		
		/// <summary>
		/// Get Create table SQL queries for all relation tables needed by specified type. 
		/// </summary>
		/// <param name="type">
		/// Entity type. <see cref="Type"/>
		/// </param>
		/// <returns>
		/// SQL queries for creating tables for relation tables of specified type. 
		/// Empty string, if there are no many-to-many relations in specified type.
		/// </returns>
		private string GetCreateRelationTableQueriesForType(Type type)
		{
			var properties = SqlitePersistentProperty.GetPersistentProperties(type);
			var manyRelationProperties = from p in properties where p.IsManyRelation select p;
			
			StringBuilder query = new StringBuilder();
			
			foreach(var relation in manyRelationProperties)
			{
				query.AppendLine("CREATE TABLE [" + type.FullName + "." + relation.PersistentName + "]");
				query.AppendLine("(");	
				query.AppendLine("\t[Source Id] INTEGER NOT NULL,\n\t[Destination Id] INTEGER NOT NULL");
				query.AppendLine(");");
			}
			
			return query.ToString();
		}
		
		/// <summary>
		/// Create index tables for TypeCoordinator. These tables are used only
		/// by TypeCoordinator. TypeCoordinator saves all known types and their
		/// properties into these tables. Data is loaded from tables into memory
		/// when object is created.
		/// </summary>
		private void CreateIndexTables()
		{
			try
			{
				string queryForTypes = 
					@"
						CREATE TABLE [" + this.KnownEntitiesTable + @"]
						(
							Id INTEGER PRIMARY KEY,
							Name TEXT,
							Version REAL
						);
					";
				
				string queryForProperties = 
					@"
						CREATE TABLE [" + this.KnownPropertiesTable + @"]
						(
							[Type Id] INTEGER,
							Name TEXT,
							PersistentName TEXT,
							Type TEXT,
							FOREIGN KEY ([Type Id]) REFERENCES [" + this.KnownEntitiesTable + @"](Id)
						);
					";
				
				// Create _KnownEntityTypes table.
				using(SqliteCommand command = (SqliteCommand) this.connection.CreateCommand())
				{
					command.CommandText = queryForTypes;
					EntifyLogger.LogQuery(command.GetQuery());
					command.ExecuteNonQuery();
				}
				
				// Create _KnownEntityProperties table.
				using(SqliteCommand command = (SqliteCommand) this.connection.CreateCommand())
				{
					command.CommandText = queryForProperties;
					EntifyLogger.LogQuery(command.GetQuery());
					command.ExecuteNonQuery();
				}
			}
			catch(SqliteException e)
			{
				throw new AlreadyExistsException("Index tables exists already", e);	
			}
		}
		
		/// <summary>
		/// Add new type to index tables.
		/// </summary>
		/// <param name="type">
		/// Type to be added. <see cref="Type"/>
		/// </param>
		private void AddTypeToIndexTables(Type type)
		{
			long typeId;
			
			// Insert type information to known types table. Insert returns auto inceremented id of the inserted row.
			using(SqliteCommand command = (SqliteCommand) this.connection.CreateCommand())
			{
				command.CommandText = "INSERT INTO [" + this.KnownEntitiesTable + @"](Name, Version) 
									   VALUES (@name, @version); 
									   SELECT last_insert_rowid() AS Id;";
				
				command.Parameters.AddWithValue("@name", type.FullName);
				command.Parameters.AddWithValue("@version", AttributeReader.GetEntityVersion(type));
				typeId = (long)command.ExecuteScalar();
			}

			using (SqliteTransaction transaction = (SqliteTransaction) this.connection.BeginTransaction())
			{
				using(SqliteCommand command = (SqliteCommand) this.connection.CreateCommand())
				{
					command.CommandText = "INSERT INTO [" + this.KnownPropertiesTable + "]([Type Id], Name, PersistentName, Type) VALUES (@id, @name, @persistentname, @type)";
					
					List<SqlitePersistentProperty> properties = SqlitePersistentProperty.GetPersistentProperties(type);

					foreach(var p in properties)
					{
						command.Parameters.AddWithValue("@id", typeId);
						command.Parameters.AddWithValue("@name", p.Name);
						command.Parameters.AddWithValue("@persistentname", p.PersistentName);
						command.Parameters.AddWithValue("@type", p.Type.FullName);
						EntifyLogger.LogQuery(command.GetQuery());
						
						try
						{	
							command.ExecuteNonQuery();
						}
						catch(SqliteException e)
						{
							throw new AlreadyExistsException("Entity already exists in persistent store.", e);	
						}
						
						command.Parameters.Clear();
					}
				}
				
				transaction.Commit();
			}
		}
		
		/// <summary>
		/// Load known entity types from index table.
		/// </summary>
		/// <returns>
		/// List of known types and their versions. <see cref="Dictionary<Type, System.Double>"/>
		/// </returns>
		private Dictionary<string, double> LoadKnownTypesFromDatabase()
		{
			Dictionary<string, double> types = new Dictionary<string, double>();

			using(SqliteCommand command = (SqliteCommand) connection.CreateCommand())
			{
				command.CommandText = "SELECT Name, Version FROM [" + this.KnownEntitiesTable + "]";
				EntifyLogger.LogQuery(command.GetQuery());
				
				using(SqliteDataReader reader = (SqliteDataReader) command.ExecuteReader())
				{
					while(reader.Read())
					{
						string typeName = reader.GetString("Name").Trim();
						double version = reader.GetDouble("Version");
						types.Add(typeName, version);
					}
				}
			}	
			
			return types;
		}
		
		/// <summary>
		/// Checks does table exists in Sqlite persistent store.
		/// </summary>
		/// <param name="tableName">
		/// Name of the table to be checked <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// True if table exists, otherwise false. <see cref="System.Boolean"/>
		/// </returns>
		private bool TableExists(string tableName)
		{
			if(string.IsNullOrEmpty(tableName))
			{
				throw new ArgumentException();	
			}
			
			bool tableFound = false;
			
			using(SqliteCommand command = (SqliteCommand) connection.CreateCommand())
			{
				command.CommandText = "SELECT name FROM sqlite_master WHERE name=@tablename";
				command.Parameters.AddWithValue("@tablename", tableName);
				EntifyLogger.LogQuery(command.GetQuery());
				
				using(SqliteDataReader reader = command.ExecuteReader())
				{
					if(reader.HasRows)
					{
						tableFound = true;
					}
				}
			}
			
			return tableFound;
		}
	}
}
